using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using FireStarter.Base;
using FireStarterModeller.BO;
using Habanero.Base.Exceptions;
using Habanero.Util;

namespace FireStarter.CodeGeneration.Generator
{
    public class ReflectionGenerationStrategy : IGenerationStrategy
    {
        public ReflectionGenerationStrategy()
        {
            ValueDelegates = new ValueDelegatesDictionary();
            CollectionDelegates = new CollectionDelegatesDictionary();
        }

        public ValueDelegatesDictionary ValueDelegates { get; private set; }
        public CollectionDelegatesDictionary CollectionDelegates { get; private set; }

        public object GetValue
            (IModelStrategyMapper modelStrategyMapper, IModelElement modelElement, string propertyName)
        {
            if (ValueDelegates.ContainsKey(propertyName))
            {
                ValueDelegate delegateKey = ValueDelegates[propertyName];
                return delegateKey(modelElement);
            }

            return GetReflectedValue(modelStrategyMapper, modelElement, propertyName);
        }

        public IEnumerable<IModelElement> GetCollection
            (IModelStrategyMapper modelStrategyMapper, IModelElement modelElement, string collectionName)
        {
            if (CollectionDelegates.ContainsKey(collectionName))
            {
                CollectionDelegate delegateKey = CollectionDelegates[collectionName];
                return delegateKey(modelElement);
            }

            return GetReflectedCollection(modelStrategyMapper, modelElement, collectionName);
        }


        public object GetReflectedValue
            (IModelStrategyMapper modelStrategyMapper, IModelElement modelElement, string propertyName)
        {
            if (propertyName == null) throw new ArgumentNullException("propertyName");

            bool hasCamelCaseIndicator = (!String.IsNullOrEmpty(propertyName) && propertyName[0] == '#');
            if (hasCamelCaseIndicator) propertyName = propertyName.Substring(1);

            bool hasPluralIndicator = (!String.IsNullOrEmpty(propertyName)
                                       && propertyName[propertyName.Length - 1] == '+');
            if (hasPluralIndicator) propertyName = propertyName.Substring(0, propertyName.Length - 1);

            IModelElement finalModelElement = modelElement;
            string finalPropertyName = propertyName;

            while (finalPropertyName != null && finalPropertyName.Contains("."))
            {
                string subProp = finalPropertyName.Substring(0, finalPropertyName.IndexOf("."));
                object subValue = GetValue(modelStrategyMapper, finalModelElement, subProp);
                if (subValue == null)
                {
                    throw new InvalidCastException
                        (string.Format
                             ("On a {0} identified as {1}, the property '{2}' returned null, which is not valid for a SubModelElement.",
                              finalModelElement.GetType(), finalModelElement, subProp));
                }
                CheckItemIsIModelElement(subProp, finalPropertyName, subValue);
                IModelElement subModelElement = subValue as IModelElement;
                finalModelElement = subModelElement;
                finalPropertyName = finalPropertyName.Remove(0, finalPropertyName.IndexOf(".") + 1);
            }

            object value = GetPropertyValueFromElement(finalPropertyName, finalModelElement, modelStrategyMapper);

            if (hasCamelCaseIndicator) DoCamelCaseMutation(ref value);
            if (hasPluralIndicator) DoPluralMutation(ref value);
            return value;
        }

        private static object GetPropertyValueFromElement
            (string propertyName, IModelElement modelElement, IModelStrategyMapper modelStrategyMapper)
        {
            PropertyInfo propertyInfo = GetPropertyInfoOfElement(propertyName, modelElement);
            object value;
            if (propertyInfo != null)
            {
                try
                {
                    value = propertyInfo.GetValue(modelElement, new object[] {});
                }
                catch (Exception ex)
                {
                    throw new HabaneroApplicationException("There was an error Getting the Property '" + propertyName +
                                                           "' value from the Model Element " + modelElement.ToString(), ex);
                }
            }
            else
            {
                value = TryGetValueFromParent(propertyName, modelElement, modelStrategyMapper);

                if (value == null)
                    throw new PropertyMissingException
                        (propertyName,
                         string.Format
                             ("The property '{0}' does not exist on this IModelElement of type '{1}'.", propertyName,
                              modelElement.GetType().FullName));
            }
            return value;
        }

        private static object TryGetValueFromParent
            (string propertyName, IModelElement modelElement, IModelStrategyMapper modelStrategyMapper)
        {
            if (modelStrategyMapper != null && modelElement.Parent != null)
            {
                IModelElement parent = modelElement.Parent;
                IGenerationStrategy parentGenerationStrategy = modelStrategyMapper.GetStrategy(parent.GetType());
                try
                {
                    return parentGenerationStrategy.GetValue(modelStrategyMapper, parent, propertyName);
                }
                catch (PropertyMissingException)
                {
                    //Fall through to throwing local context error below
                }
            }
            return null;
        }

        private static PropertyInfo GetPropertyInfoOfElement(string propertyName, IModelElement modelElement)
        {
            const BindingFlags bindingFlags = BindingFlags.IgnoreCase | BindingFlags.NonPublic | BindingFlags.Public
                                              | BindingFlags.Instance;
            PropertyInfo propertyInfo = null;
            foreach (Type @interface in modelElement.GetType().GetInterfaces())
            {
                bool isIModelElementInterface = false;
                if (@interface == typeof (IModelElement)) isIModelElementInterface = true;
                foreach (Type parentInterface in @interface.GetInterfaces())
                {
                    if (parentInterface == typeof (IModelElement)) isIModelElementInterface = true;
                }
                if (isIModelElementInterface)
                {
                    propertyInfo = @interface.GetProperty(propertyName, bindingFlags);
                    if (propertyInfo != null) break;
                }
            }

            if (propertyInfo == null) propertyInfo = modelElement.GetType().GetProperty(propertyName, bindingFlags);
            if (propertyInfo == null)
            {
                foreach (Type @interface in modelElement.GetType().GetInterfaces())
                {
                    propertyInfo = @interface.GetProperty(propertyName, bindingFlags);
                    if (propertyInfo != null) break;
                }
            }
            return propertyInfo;
        }

        private static void DoCamelCaseMutation(ref object value)
        {
            if (!(value is string)) return;
            string stringValue = Convert.ToString(value);
            if (String.IsNullOrEmpty(stringValue)) return;

            value = StringUtilities.ToLowerFirstLetter(stringValue);
        }

        private static void DoPluralMutation(ref object value)
        {
            if (!(value is string)) return;
            string stringValue = Convert.ToString(value);
            if (String.IsNullOrEmpty(stringValue)) return;

            value = StringUtilities.Pluralize(stringValue);
        }

        public IEnumerable<IModelElement> GetReflectedCollection
            (IModelStrategyMapper modelStrategyMapper, IModelElement modelElement, string collectionName)
        {
            object value = GetValue(modelStrategyMapper, modelElement, collectionName);
            if (value == null) return new List<IModelElement>();
            if (value is IEnumerable<IModelElement> || value.GetType().IsSubclassOf(typeof (IEnumerable<IModelElement>)))
                return (IEnumerable<IModelElement>) value;
            CheckValueIsEnumerable(collectionName, value);
            List<IModelElement> list = new List<IModelElement>(5);
            foreach (object item in (IEnumerable) value)
            {
                CheckEnumerableItemIsIModelElement(collectionName, item);
                list.Add((IModelElement) item);
            }
            return list;
        }

        private static void CheckEnumerableItemIsIModelElement(string collectionName, object item)
        {
            if (!(item is IModelElement))
                throw new InvalidCastException
                    (string.Format
                         ("The property '{0}' returned an IEnumerable but none of the items were of type IModelElement.",
                          collectionName));
        }

        private static void CheckItemIsIModelElement(string objectName, string fullPropertyName, object item)
        {
            if (!(item is IModelElement))
                throw new InvalidCastException
                    (string.Format
                         ("The property '{0}' (which is part of the full property '{1}') returned an object that is not of type IModelElement.",
                          objectName, fullPropertyName));
        }

        private static void CheckValueIsEnumerable(string collectionName, object value)
        {
            if (!(value is IEnumerable))
                throw new InvalidCastException
                    (string.Format("The property '{0}' did not return an object of type IEnumerable.", collectionName));
        }
    }
}